Skip to content
This repository has been archived by the owner on Apr 27, 2021. It is now read-only.

Created a custom rule for import-type-order #48

Merged
merged 17 commits into from
Mar 15, 2017

Conversation

ismail-syed
Copy link
Contributor

@ismail-syed ismail-syed commented Mar 10, 2017

Enforce a certain structure in your imports. Import structure should be listed in the following order:

  • Module imports
  • Absolute imports
  • Relative parent/ancestor directory imports
  • Relative sibling directory imports

Issue #42

@ismail-syed ismail-syed force-pushed the custom-rule_structured-imports branch from 26a470b to 73d9b7a Compare March 10, 2017 14:03
@ismail-syed ismail-syed self-assigned this Mar 10, 2017
}
}

function getImportType(path: string): ImportType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestions on making this look a little cleaner/prettier?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, I'd go the other way and make it uglier. It's not going to expand beyond 4 possibilities, and most devs grok these hardcoded strings faster than pretty method names.

if (path.substr(1, 2) === './') {
  return Import.Sibling;
} else if (path.substr(1, 4) === '../') {

If I was going to make it prettier, I'd try adding a static toType(path: string) function to the enum.

Copy link
Member

@GoodForOneFare GoodForOneFare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start! Lots of comments, but nothing major.

}

// e.g. 'import Foo from './foo';'
public visitImportDeclaration(node: ts.ImportDeclaration) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So normally this would be cool, but I'm really concerned about performance. yarn check is taking over 2 minutes to run (>40% of CI time), and that'll only get worse as we add more code + rules. AFAIK, there's no way to parallelize tslint.

Taking a look at the tslint performance guide, it recommends:

  • Don’t walk the whole AST if possible
  • Implement your own walking algorithm

If you are interested in import statements for example, you only need to search in sourceFile.statements and nested NamespaceDeclaration

... and since nested declarations can't do module imports, that limits this rule to sourceFile.statements.

Here's a rough version of the above, with annotations on how to improve it. It passes for the existing tests, at least.

// This should be adapted to `extend Lint.AbstractWalker<MyOptionsType>`, and implement the `walk` rule.
class StructuredImportsWalker extends Lint.RuleWalker {
  private previousImport: ImportType;
  private currentImport: ImportType;

  constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
    super(sourceFile, options);
  }

  public visitSourceFile(node: ts.SourceFile) {
    const importNodes = node.statements
      .filter((child) => child.kind === ts.SyntaxKind.ImportDeclaration)
      .map((child) => child as ts.ImportDeclaration);

    if (importNodes.length === 0) { return; }

    // This is off the top of my head; probably a more readable way to do this.
    let lastGoodType = getImportType(importNodes.shift());
    while (importNodes.length) {
      const currentImport = importNodes.shift();
      const currentType = getImportType(currentImport);
      // Here we're just relying on enum comparisons.  This gets rid of a couple of helper functions.
      if (lastGoodType > currentType) {
        this.addFailure(
          this.createFailure(
            currentImport.moduleSpecifier.getStart(), 
            currentImport.moduleSpecifier.getFullWidth(),
            Rule.STRUCTURED_IMPORTS_ABS_FIRST_ERROR,
          )
        );
      } else {
        lastGoodType = currentType;
      }
    }
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback!
I couldn't run my tests with this, earlier today. It needed a bit more refactoring but it was a great starting point.
Did you build then run the tests? I think that might of done it, it gets me all the time too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I was using this to run the test:

yarn run build && $(npm bin)/tslint -r ./build/customRules/ --test test/rules/structured-imports/

The above code was a tweaked version of this ugly stuff (that definitely passes)... I'll hide this in a details block:

import * as ts from 'typescript';
import * as Lint from 'tslint';

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys /
public static metadata: Lint.IRuleMetadata = {
'ruleName': 'structured-imports',
'description': 'Enforce structure to your imports. Import structure should be listed in the folloing order: modules, absolute imports, relative parent/ancestor directories, relative sibling directors.',
'hasFix': false,
'optionsDescription': 'Not configurable.',
'options': null,
'optionExamples': null,
'type': 'style',
'typescriptOnly': false,
};
/
tslint:enable:object-literal-sort-keys */

public static STRUCTURED_IMPORTS_ABS_FIRST_ERROR = 'Imports should be listed in the following order: module imports, absolute imports, relative imports.';

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const structuredImportsWalker = new StructuredImportsWalker(sourceFile, this.getOptions());
return this.applyWithWalker(structuredImportsWalker);
}
}

enum ImportType {
Module = 0, // 'module'
Absolute = 1, // '/some/folder/file'
RelativeAncestor = 2, // '../parentFolder'
RelativeSibling = 3, // './siblingFolder'
};

const importStuctureOrder = [ImportType.Module, ImportType.Absolute, ImportType.RelativeAncestor, ImportType.RelativeSibling];

class StructuredImportsWalker extends Lint.RuleWalker {
private previousImport: ImportType;
private currentImport: ImportType;

constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options);
}

public visitSourceFile(node: ts.SourceFile) {
const importNodes = node.statements
.filter((child) => child.kind === ts.SyntaxKind.ImportDeclaration)
.map((child) => child as ts.ImportDeclaration);

if (importNodes.length === 0) { return; }

let lastGoodType = getImportType(importNodes.shift());
while (importNodes.length) {
  const currentImport = importNodes.shift();
  const currentType = getImportType(currentImport);
  if (lastGoodType > currentType) {
    this.addFailure(
      this.createFailure(
        currentImport.moduleSpecifier.getStart(), 
        currentImport.moduleSpecifier.getFullWidth(),
        Rule.STRUCTURED_IMPORTS_ABS_FIRST_ERROR,
      )
    );
  } else {
    lastGoodType = currentType;
  }
}

}
}

function getImportType({moduleSpecifier}: ts.ImportDeclaration): ImportType {
const path = moduleSpecifier.getText();
if (isRelativeSibling(path)) {
return ImportType.RelativeSibling;
} else if (isRelativeAncestor(path)) {
return ImportType.RelativeAncestor;
} else if (isAbsolute(path)) {
return ImportType.Absolute;
}
return ImportType.Module;
}

function isAbsolute(path: string) {
return path[1] === '/' ; // [0] is quote mark
}

function isRelativeAncestor(path: string) {
return path.substr(1, 4) === '../' ; // path[0] is quote mark
}

function isRelativeSibling(path: string) {
return path.substr(1, 2) === './' ; // path[0] is quote mark
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, must of done something wrong when I was trying this our earlier today.

# Enforces a certain structure in your import statements

## Rationale
- Consistency in import statement structure throughout the codebase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This states a Shopify-specific end goal rather than a rationale. Looking through other rationales, adjacent-overload-signature has this:

Improves readability and organization by grouping naturally related items together.

That looks good to me.

- Consistency in import statement structure throughout the codebase

## Rule Details
Enforce a certain structure in your imports. Import structure should be listed in the following order:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is vague/flowery.

Imports should be in the following order:
* Externals
* Absolute paths
* Parent directories
* Siblings

Note that I'm borrowing "externals" from the eslint import/order rule. Everything in TypeScript is a module, so a more specific term is useful.

import BarModule from 'BarModule';
```

The following patterns are not warnings:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer: The following import order is valid:

Sometimes coding rules apply to English! Less cognitive load to interpret "valid" than "not warnings".


## When Not To Use It

If you do not with to enforce consistency in your import structuring.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: with=>wish.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

structuring is flowery; the sentence reads the same without it.

@@ -0,0 +1,10 @@
import Foo from './foo';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I've no idea what multiLine is testing. This rule will never fail on a single import ¯\(ツ)

'options': null,
'optionExamples': null,
'type': 'style',
'typescriptOnly': false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't care about JS, so let's make this true.

/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
'ruleName': 'structured-imports',
'description': 'Enforce structure to your imports. Import structure should be listed in the folloing order: modules, absolute imports, relative parent/ancestor directories, relative sibling directors.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: folloing=>following.

enum ImportType {
Module = 0, // 'module'
Absolute = 1, // '/some/folder/file'
RelativeAncestor = 2, // '../parentFolder'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RelativeAncestor=>Ancestor (relative is implied).

Module = 0, // 'module'
Absolute = 1, // '/some/folder/file'
RelativeAncestor = 2, // '../parentFolder'
RelativeSibling = 3, // './siblingFolder'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RelativeSibling=>Sibling.

const importStuctureOrder = [ImportType.External, ImportType.Absolute, ImportType.Ancestor, ImportType.Sibling];

class StructuredImportsWalker extends Lint.AbstractWalker<void> {
private previousImport: ImportType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of making the walker stateful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woah, that was fast!
There's no benefit to it being stateful. I was still refactoring, just pushed whatever I had that was in a 'happy state'.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, right! Happy states are good practice. Sorry, too quick on the trigger 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No no, its okay. I appreciate the helpful comments more than you think!

@ismail-syed ismail-syed force-pushed the custom-rule_structured-imports branch from 3d82bcf to 408a9a0 Compare March 14, 2017 02:13
@@ -57,7 +57,7 @@ function walker(context: Lint.WalkContext<void>): void {
function getImportType(path: string): ImportType {
if (path.substr(1, 2) === './') {
return ImportType.Sibling;
} else if (path.substr(1, 4) === '../') {
} else if (path.substr(1, 3) === '../') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The granular test cases are doing their job! 👍

@ismail-syed ismail-syed changed the title Created a custom rule for structured-imports Created a custom rule for import-type-order Mar 14, 2017
@ismail-syed
Copy link
Contributor Author

@GoodForOneFare All the changes are addressed. Have another look and let me know what you think.

I've created an issue for creating custom Rule fixes. I'll add those as separate PR's in the near future, so we can keep things moving with this PR.

Copy link
Member

@GoodForOneFare GoodForOneFare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just minor comments. With some quick cleanup, we'll be able to ship this today 🎉

@@ -0,0 +1,4 @@
import {Foo, Bar, Baz} from 'FooBarBaz';
import BazTwo from '/Baz';
import FooTwo from '../foo';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a side-effecty import somewhere in here, please?

- Improves readability and organization by grouping naturally related items together.

## Rule Details
Enforce order of import statement types. Import structure should be listed in the following order:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

structure isn't adding anything to this sentence.


The following are considered warnings
```js
import Foo from './foo';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some comments explaining the nature of each offence. e.g.,

import Foo from './foo'; // Should come after external and parent imports.

enum ImportType {
External = 0, // 'external'
Absolute = 1, // '/some/folder/file'
Ancestor = 2, // '../parentFolder'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still on the fence about Ancestor, but I can't think of anything better. Internal? Local? No. Bah.

Ohwell, they're not used outside this file, so we can easily change when inspiration strikes 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's annoying because parents and ancestors are both valid imports for this type. I think Ancestor is the best name, given all our options.


const importStuctureOrder = [ImportType.External, ImportType.Absolute, ImportType.Ancestor, ImportType.Sibling];

function walker(context: Lint.WalkContext<void>): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

}
}

function getImportType(path: string): ImportType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just accept getImportType({moduleSpecifier}: ts.ImportDeclaration), and fish out the text in here.

It's a minor reduction in the walker's verbosity, but every little bit helps.


let previousImport = getImportType(importNodes.shift().moduleSpecifier.getText());
while (importNodes.length) {
const currentImportNode = importNodes.shift().moduleSpecifier;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moduleSpecifier is of type StringLiteral, so this should be currentPath or currentModuleSpecifier.

let previousImport = getImportType(importNodes.shift().moduleSpecifier.getText());
while (importNodes.length) {
const currentImportNode = importNodes.shift().moduleSpecifier;
const currentImport = getImportType(currentImportNode.getText());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentType would be more accurate.


if (importNodes.length === 0) { return; }

let previousImport = getImportType(importNodes.shift().moduleSpecifier.getText());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previous implies that the loop is just dumbly assigning to this var, but it's only assigning types for valid imports.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and it's not an import. It's a lastValidType.

@GoodForOneFare
Copy link
Member

A CHANGELOG update would be nice, too.

@ismail-syed
Copy link
Contributor Author

@GoodForOneFare I think we're good now! 🤞

@ismail-syed
Copy link
Contributor Author

ismail-syed commented Mar 14, 2017

Ewww. Thought I'd give the GitHub web ui a shot for fixing merge conflicts.
It left a merge commit Merge branch 'master' into custom-rule_structured-imports.
Pretty convenient though.

Copy link
Member

@GoodForOneFare GoodForOneFare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix more pedantry and 🚢 👍


```js
export interface TheThreeStooges {
import BarModule from 'BarModule';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imports should be indented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I don't even know why I wrapped those imports in an interface. Probably pulled code from somewhere and forgot to get rid of the export.

/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
'ruleName': 'import-type-order',
'description': 'Enforce structure to your imports. Import structure should be listed in the following order: modules, absolute imports, relative parent/ancestor directories, relative sibling directors.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improves readability and organization by grouping related imports together. Imports should be listed in order of: external modules, absolute paths, relative paths, relative siblings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should've been fixed.

};
/* tslint:enable:object-literal-sort-keys */

public static IMPORT_TYPE_ORDER_ERROR = 'Imports should be listed in the following order: module imports, absolute imports, ancestor imports, sibling imports.';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/module/external

@ismail-syed ismail-syed force-pushed the custom-rule_structured-imports branch from 0751d80 to f1a2311 Compare March 15, 2017 02:06
@ismail-syed ismail-syed force-pushed the custom-rule_structured-imports branch from f1a2311 to 97f6669 Compare March 15, 2017 02:08
@ismail-syed ismail-syed merged commit 60ec7b8 into master Mar 15, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants